package com.example.auth.authorization.dao;

import com.example.auth.authorization.constants.ParameterNamesEmail;
import com.example.auth.authorization.constants.ParameterNamesSms;
import com.example.auth.entity.User;
import com.example.auth.exception.InvalidCaptchaException;
import com.example.auth.service.AuthUserService;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper;
import org.springframework.security.core.userdetails.*;
import org.springframework.security.core.userdetails.cache.NullUserCache;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

@Slf4j
@Component
public class FormUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware {
    @Resource
    AuthUserService authUserService;

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";

    public static final String PARAM_TYPE = "type";

    protected final Log logger = LogFactory.getLog(getClass());

    protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();

    private UserCache userCache = new NullUserCache();

    private boolean forcePrincipalAsString = false;

    protected boolean hideUserNotFoundExceptions = true;

    private UserDetailsChecker preAuthenticationChecks = new FormUserDetailsAuthenticationProvider.DefaultPreAuthenticationChecks();

    private UserDetailsChecker postAuthenticationChecks = new FormUserDetailsAuthenticationProvider.DefaultPostAuthenticationChecks();

    private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();

    private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";

    @Resource
    private PasswordEncoder passwordEncoder;

    private volatile String userNotFoundEncodedPassword;
    @Resource

    private UserDetailsService userDetailsService;

    private UserDetailsPasswordService userDetailsPasswordService;


    /**
     * Creates a new instance using the provided {@link PasswordEncoder}
     *
     * @param passwordEncoder the {@link PasswordEncoder} to use. Cannot be null.
     * @since 6.0.3
     */
    public FormUserDetailsAuthenticationProvider(PasswordEncoder passwordEncoder, UserDetailsService userDetailsService) {
        setPasswordEncoder(passwordEncoder);
        setUserDetailsService(userDetailsService);
    }

    public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
        Assert.notNull(passwordEncoder, "passwordEncoder cannot be null");
        this.passwordEncoder = passwordEncoder;
        this.userNotFoundEncodedPassword = null;
    }


    public void setUserDetailsService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    protected UserDetailsService getUserDetailsService() {
        return this.userDetailsService;
    }



    /**
     * Allows subclasses to perform any additional checks of a returned (or cached)
     * <code>UserDetails</code> for a given authentication request. Generally a subclass
     * will at least compare the {@link Authentication#getCredentials()} with a
     * {@link UserDetails#getPassword()}. If custom logic is needed to compare additional
     * properties of <code>UserDetails</code> and/or
     * <code>UsernamePasswordAuthenticationToken</code>, these should also appear in this
     * method.
     *
     * @param userDetails    as retrieved from the
     *                       {@link #retrieveUser(String, UsernamePasswordAuthenticationToken)} or
     *                       <code>UserCache</code>
     * @param authentication the current request that needs to be authenticated
     * @throws AuthenticationException AuthenticationException if the credentials could
     *                                 not be validated (generally a <code>BadCredentialsException</code>, an
     *                                 <code>AuthenticationServiceException</code>)
     */
    protected void additionalAuthenticationChecks(UserDetails userDetails,
                                                  UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        if (authentication.getCredentials() == null) {
            this.logger.debug("Failed to authenticate since no credentials provided");
            throw new BadCredentialsException(this.messages
                    .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "凭证错误"));
        }
        String presentedPassword = authentication.getCredentials().toString();
        if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
            this.logger.debug("Failed to authenticate since password does not match stored value");
            throw new BadCredentialsException(this.messages
                    .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "凭证错误"));
        }
    }

    @Override
    public final void afterPropertiesSet() throws Exception {
        Assert.notNull(this.userCache, "A user cache must be set");
        Assert.notNull(this.messages, "A message source must be set");
        doAfterPropertiesSet();
    }

    protected void doAfterPropertiesSet() {
        Assert.notNull(this.userDetailsService, "A UserDetailsService must be set");
    }

    protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        prepareTimingAttackProtection();
        try {
            UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
            if (loadedUser == null) {
                throw new InternalAuthenticationServiceException(
                        "UserDetailsService returned null, which is an interface contract violation");
            }
            return loadedUser;
        } catch (UsernameNotFoundException ex) {
            mitigateAgainstTimingAttack(authentication);
            throw ex;
        } catch (InternalAuthenticationServiceException ex) {
            throw ex;
        } catch (Exception ex) {
            throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
        }
    }

    private void prepareTimingAttackProtection() {
        if (this.userNotFoundEncodedPassword == null) {
            this.userNotFoundEncodedPassword = this.passwordEncoder.encode(USER_NOT_FOUND_PASSWORD);
        }
    }

    private void mitigateAgainstTimingAttack(UsernamePasswordAuthenticationToken authentication) {
        if (authentication.getCredentials() != null) {
            String presentedPassword = authentication.getCredentials().toString();
            this.passwordEncoder.matches(presentedPassword, this.userNotFoundEncodedPassword);
        }
    }

    protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
                                                         UserDetails user) {
        boolean upgradeEncoding = this.userDetailsPasswordService != null
                && this.passwordEncoder.upgradeEncoding(user.getPassword());
        if (upgradeEncoding) {
            String presentedPassword = authentication.getCredentials().toString();
            String newPassword = this.passwordEncoder.encode(presentedPassword);
            user = this.userDetailsPasswordService.updatePassword(user, newPassword);
        }
        UsernamePasswordAuthenticationToken result = UsernamePasswordAuthenticationToken.authenticated(principal,
                authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
        result.setDetails(authentication.getDetails());
        this.logger.debug("Authenticated user");
        return result;
    }


    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        boolean cacheWasUsed = true;
        UserDetails userDetails = null;

        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        if (requestAttributes == null) {
            throw new InvalidCaptchaException("Failed to get the current request.");
        }
        HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
        String type = request.getParameter(PARAM_TYPE);
        if (ParameterNamesEmail.GRANT_TYPE.equals(type)) {
            String captcha = request.getParameter(ParameterNamesEmail.CAPTCHA);
            String email = request.getParameter(ParameterNamesEmail.EMAIL);
            Object redisCaptcha = redisTemplate.opsForValue().getAndDelete(email + ":captcha:" + captcha);
            if (redisCaptcha == null) {
                OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
                        "认证失败：邮箱或验证码错误", ERROR_URI);
                throw new OAuth2AuthenticationException(error);
            } else {
                com.example.auth.entity.User user = authUserService.getUserByEmail(email);
                try {
                    authentication = UsernamePasswordAuthenticationToken.unauthenticated(user.getUsername(), user.getPassword());
                    userDetails = this.userCache.getUserFromCache(user.getUsername());
                    if (userDetails == null) {
                        cacheWasUsed = false;
                        try {
                            userDetails = retrieveUser(user.getUsername(), (UsernamePasswordAuthenticationToken) authentication);
                        } catch (UsernameNotFoundException ex) {
                            this.logger.debug("Failed to find user '" + user.getUsername() + "'");
                            if (!this.hideUserNotFoundExceptions) {
                                throw ex;
                            }
                            throw new BadCredentialsException(this.messages
                                    .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "凭证错误"));
                        }
                        Assert.notNull(userDetails, "retrieveUser returned null - a violation of the interface contract");
                    }
                } catch (Exception e) {
                    throw new InvalidCaptchaException("用户不存在");
                }
            }

        } else if (ParameterNamesSms.GRANT_TYPE.equals(type)) {
            String phone = request.getParameter(ParameterNamesSms.PHONE);
            String captcha = request.getParameter(ParameterNamesSms.CAPTCHA);
            Object redisCaptcha = redisTemplate.opsForValue().getAndDelete(phone + ":captcha:" + captcha);
            if (redisCaptcha == null) {
                OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
                        "认证失败：手机号或验证码错误", ERROR_URI);
                throw new OAuth2AuthenticationException(error);
            } else {
                User user = authUserService.getUserByPhone(phone);
                try {
                    authentication = UsernamePasswordAuthenticationToken.unauthenticated(user.getUsername(), user.getPassword());
                    userDetails = this.userCache.getUserFromCache(user.getUsername());
                    if (userDetails == null) {
                        cacheWasUsed = false;
                        try {
                            userDetails = retrieveUser(user.getUsername(), (UsernamePasswordAuthenticationToken) authentication);
                        } catch (UsernameNotFoundException ex) {
                            this.logger.debug("Failed to find user '" + user.getUsername() + "'");
                            if (!this.hideUserNotFoundExceptions) {
                                throw ex;
                            }
                            throw new BadCredentialsException(this.messages
                                    .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "凭证错误"));
                        }
                        Assert.notNull(userDetails, "retrieveUser returned null - a violation of the interface contract");
                    }
                } catch (Exception e) {
                    throw new InvalidCaptchaException("用户不存在");
                }

            }
        } else {
            Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
                    () -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
                            "Only UsernamePasswordAuthenticationToken is supported"));
            String username = determineUsername(authentication);
            cacheWasUsed = true;
            userDetails = this.userCache.getUserFromCache(username);
            if (userDetails == null) {
                cacheWasUsed = false;
                try {
                    userDetails = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
                } catch (UsernameNotFoundException ex) {
                    this.logger.debug("Failed to find user '" + username + "'");
                    if (!this.hideUserNotFoundExceptions) {
                        throw ex;
                    }
                    throw new BadCredentialsException(this.messages
                            .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "凭证错误"));
                }
                Assert.notNull(userDetails, "retrieveUser returned null - a violation of the interface contract");
            }
        }
        return getAuthentication(authentication, userDetails, cacheWasUsed);
    }

    private Authentication getAuthentication(Authentication authentication, UserDetails user, boolean cacheWasUsed) {

        try {
            this.preAuthenticationChecks.check(user);
            additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
        }
        catch (AuthenticationException ex) {
            if (!cacheWasUsed) {
                throw ex;
            }
            // There was a problem, so try again after checking
            // we're using latest data (i.e. not from the cache)
            cacheWasUsed = false;
            user = retrieveUser(authentication.getPrincipal().toString(), (UsernamePasswordAuthenticationToken) authentication);
            this.preAuthenticationChecks.check(user);
            additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
        }
        this.postAuthenticationChecks.check(user);

        if (!cacheWasUsed) {
            this.userCache.putUserInCache(user);
        }
        Object principalToReturn = user;
        if (this.forcePrincipalAsString) {
            principalToReturn = user.getUsername();
        }
        return createSuccessAuthentication(principalToReturn, authentication, user);
    }

    private String determineUsername(Authentication authentication) {
        return (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();
    }






    @Override
    public void setMessageSource(MessageSource messageSource) {
        this.messages = new MessageSourceAccessor(messageSource);
    }



    @Override
    public boolean supports(Class<?> authentication) {
        return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
    }



    private class DefaultPreAuthenticationChecks implements UserDetailsChecker {

        @Override
        public void check(UserDetails user) {
            if (!user.isAccountNonLocked()) {
                FormUserDetailsAuthenticationProvider.this.logger
                        .debug("Failed to authenticate since user account is locked");
                throw new LockedException(FormUserDetailsAuthenticationProvider.this.messages
                        .getMessage("AbstractUserDetailsAuthenticationProvider.locked", "User account is locked"));
            }
            if (!user.isEnabled()) {
                FormUserDetailsAuthenticationProvider.this.logger
                        .debug("Failed to authenticate since user account is disabled");
                throw new DisabledException(FormUserDetailsAuthenticationProvider.this.messages
                        .getMessage("AbstractUserDetailsAuthenticationProvider.disabled", "User is disabled"));
            }
            if (!user.isAccountNonExpired()) {
                FormUserDetailsAuthenticationProvider.this.logger
                        .debug("Failed to authenticate since user account has expired");
                throw new AccountExpiredException(FormUserDetailsAuthenticationProvider.this.messages
                        .getMessage("AbstractUserDetailsAuthenticationProvider.expired", "User account has expired"));
            }
        }

    }

    private class DefaultPostAuthenticationChecks implements UserDetailsChecker {

        @Override
        public void check(UserDetails user) {
            if (!user.isCredentialsNonExpired()) {
                FormUserDetailsAuthenticationProvider.this.logger
                        .debug("Failed to authenticate since user account credentials have expired");
                throw new CredentialsExpiredException(FormUserDetailsAuthenticationProvider.this.messages
                        .getMessage("AbstractUserDetailsAuthenticationProvider.credentialsExpired",
                                "User credentials have expired"));
            }
        }

    }
}
